/* * Copyright (C) 2012 Jason Gedge <http://www.gedge.ca> * * This file is part of the OpGraph project. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.gedge.opgraph.app.components.library; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Point; import java.awt.RenderingHints; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.font.FontRenderContext; import java.awt.font.LineMetrics; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.HashSet; import java.util.List; import java.util.logging.Logger; import javax.swing.ButtonGroup; import javax.swing.JEditorPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JRadioButtonMenuItem; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTree; import javax.swing.border.EmptyBorder; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeExpansionListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.StyleSheet; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import ca.gedge.opgraph.OpGraph; import ca.gedge.opgraph.OpNode; import ca.gedge.opgraph.app.GraphDocument; import ca.gedge.opgraph.app.GraphEditorModel; import ca.gedge.opgraph.app.components.ErrorDialog; import ca.gedge.opgraph.app.components.SearchField; import ca.gedge.opgraph.app.edits.graph.AddNodeEdit; import ca.gedge.opgraph.app.util.ObjectSelection; import ca.gedge.opgraph.library.NodeData; import ca.gedge.opgraph.library.NodeLibrary; import ca.gedge.opgraph.library.handlers.ClassHandler; import ca.gedge.opgraph.util.ServiceDiscovery; /** * A panel to display the node types available in a {@link NodeLibrary}. */ public class NodeLibraryViewer extends JPanel { /** Logger */ private static final Logger LOGGER = Logger.getLogger(NodeLibraryViewer.class.getName()); /** The library this panel is viewing */ private NodeLibrary library; /** The search field */ private SearchField filterField; /** The list of nodes in the library */ private JTree libraryTree; /** The tree model being used */ private NodeLibraryTreeModel model; /** Tree renderer being used */ private DefaultTreeCellRenderer renderer; /** A component showing information on the selected item */ private JEditorPane infoPane; /** A list of the rows that are expanded */ private HashSet<String> expandedCategories; /** * An action and action listener that updates the filter of this node * library viewer. */ private class UpdateFilterAction implements ActionListener { private NodeInfoFilter filter; /** * */ public UpdateFilterAction() { this.filter = new NodeInfoFullTextFilter(); } @Override public void actionPerformed(ActionEvent e) { if(e != null) { // Determine what command we have if("fulltext".equals(e.getActionCommand())) { this.filter = new NodeInfoFullTextFilter(); this.filter.setFilter(filterField.getText()); } else if("name".equals(e.getActionCommand())) { this.filter = new NodeInfoNameFilter(); this.filter.setFilter(filterField.getText()); } else if("description".equals(e.getActionCommand())) { this.filter = new NodeInfoDescriptionFilter(); this.filter.setFilter(filterField.getText()); } else if("category".equals(e.getActionCommand())) { this.filter = new NodeInfoCategoryFilter(); this.filter.setFilter(filterField.getText()); } else if("filter".equals(e.getActionCommand())) { this.filter.setFilter(filterField.getText()); } // Update model final NodeLibrary library = NodeLibraryViewer.this.library; model = new NodeLibraryTreeModel(library, filter); libraryTree.setModel(model); final int count = model.getChildCount(model.getRoot()); for(int index = 0; index < count; ++index) { final Object obj = model.getChild(model.getRoot(), index); final DefaultMutableTreeNode node = (DefaultMutableTreeNode)obj; if(expandedCategories.contains(node.getUserObject())) libraryTree.expandPath(new TreePath(model.getPathToRoot(node))); } } } } /** */ private UpdateFilterAction updateFilterAction = new UpdateFilterAction(); /** * Constructs a viewer for the node library. */ public NodeLibraryViewer() { // Grab all OpNode providers and add to library final NodeLibrary library = new NodeLibrary(); library.addURIHandler(new ClassHandler()); final List<Class<? extends OpNode>> providers = ServiceDiscovery.getInstance().findProviders(OpNode.class); for(Class<? extends OpNode> provider : providers) { try { library.register(provider); } catch(Throwable exc) { LOGGER.warning("Could not register OpNode provider: " + provider); } } // Initialize component initializeComponents(library); } /** * Constructs a viewer for a specified node library. * * @param library the library to view */ public NodeLibraryViewer(NodeLibrary library) { initializeComponents(library); } private void initializeComponents(NodeLibrary library) { setLayout(new BorderLayout()); this.renderer = new DefaultTreeCellRenderer(); this.renderer.setOpenIcon(null); this.renderer.setLeafIcon(null); this.renderer.setClosedIcon(null); this.expandedCategories = new HashSet<String>(); this.libraryTree = new JTree(); this.infoPane = new JEditorPane(); this.filterField = new SearchField("Enter Filter Text"); libraryTree.setRootVisible(false); libraryTree.setEditable(false); libraryTree.setBackground(Color.WHITE); libraryTree.setShowsRootHandles(true); libraryTree.setRowHeight(-1); libraryTree.setCellRenderer(renderer); libraryTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); libraryTree.addTreeExpansionListener(new TreeExpansionListener() { @Override public void treeExpanded(TreeExpansionEvent e) { if(e.getPath().getPathCount() >= 2) { final DefaultMutableTreeNode node = (DefaultMutableTreeNode)e.getPath().getPathComponent(1); if(node != null && (node.getUserObject() instanceof String)) expandedCategories.add((String)node.getUserObject()); } } @Override public void treeCollapsed(TreeExpansionEvent e) { if(e.getPath().getPathCount() >= 2) { final DefaultMutableTreeNode node = (DefaultMutableTreeNode)e.getPath().getPathComponent(1); if(node != null && (node.getUserObject() instanceof String)) expandedCategories.remove(node.getUserObject()); } } }); libraryTree.addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { final DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)e.getPath().getLastPathComponent(); if(selectedNode != null) { final Object selectedObject = selectedNode.getUserObject(); if(selectedObject != null && selectedObject instanceof NodeData) { final NodeData info = (NodeData)selectedObject; infoPane.setText(getHTMLForNodeInfo(info)); } } } }); libraryTree.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if(e.getClickCount() == 2 && libraryTree.getSelectionPath() != null) { final DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)libraryTree.getSelectionPath().getLastPathComponent(); if(selectedNode != null) { final Object selectedObject = selectedNode.getUserObject(); if(selectedObject != null && selectedObject instanceof NodeData) { final NodeData info = (NodeData)selectedObject; final GraphDocument document = GraphEditorModel.getActiveDocument(); if(document != null) { try { final OpGraph graph = document.getGraph(); document.getUndoSupport().postEdit(new AddNodeEdit(graph, info, -1, -1)); } catch(InstantiationException exc) { final String message = "Unable to create '" + info.name + "'"; LOGGER.severe(message); ErrorDialog.showError(exc, message); } } } } } } }); setLibrary(library); // Create the filter field context menu final JRadioButtonMenuItem fullTextItem = new JRadioButtonMenuItem("Full Text"); final JRadioButtonMenuItem nameItem = new JRadioButtonMenuItem("Name"); final JRadioButtonMenuItem descriptionItem = new JRadioButtonMenuItem("Description"); final JRadioButtonMenuItem categoryItem = new JRadioButtonMenuItem("Category"); fullTextItem.setActionCommand("fulltext"); fullTextItem.addActionListener(updateFilterAction); fullTextItem.setSelected(true); nameItem.setActionCommand("name"); nameItem.addActionListener(updateFilterAction); descriptionItem.setActionCommand("description"); descriptionItem.addActionListener(updateFilterAction); categoryItem.setActionCommand("category"); categoryItem.addActionListener(updateFilterAction); final ButtonGroup group = new ButtonGroup(); group.add(fullTextItem); group.add(nameItem); group.add(descriptionItem); group.add(categoryItem); final JPopupMenu filterPopup = new JPopupMenu(); filterPopup.add(fullTextItem); filterPopup.addSeparator(); filterPopup.add(nameItem); filterPopup.add(descriptionItem); filterPopup.add(categoryItem); filterField.setContextPopup(filterPopup); // Filter field initialization filterField.getDocument().addDocumentListener(new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { final ActionEvent ae = new ActionEvent(filterField.getDocument(), 0, "filter"); updateFilterAction.actionPerformed(ae); } @Override public void insertUpdate(DocumentEvent e) { final ActionEvent ae = new ActionEvent(filterField.getDocument(), 0, "filter"); updateFilterAction.actionPerformed(ae); } @Override public void changedUpdate(DocumentEvent e) {} }); // Node information pane final StyleSheet style = new StyleSheet(); style.addRule("body { background: white; font: 12pt sans-serif; }"); style.addRule("h1 { font: bold 16pt; margin: 5px; }"); style.addRule("p { margin: 5px 10px; }"); final HTMLEditorKit htmlKit = new HTMLEditorKit(); htmlKit.setStyleSheet(style); infoPane.setEditable(false); infoPane.setEditorKit(htmlKit); // Drag support for creation of nodes DragSource.getDefaultDragSource() .createDefaultDragGestureRecognizer(libraryTree, DnDConstants.ACTION_COPY, gestureListener); // Search field and library tree on the left final JPanel searchFieldPanel = new JPanel(new BorderLayout()); searchFieldPanel.setOpaque(false); searchFieldPanel.add(filterField); searchFieldPanel.setBorder(new EmptyBorder(5, 2, 5, 2)); final JScrollPane libraryScrollPane = new JScrollPane(libraryTree); libraryScrollPane.setBorder(null); final JPanel libraryPanel = new JPanel(new BorderLayout()); libraryPanel.setBackground(Color.WHITE); libraryPanel.add(searchFieldPanel, BorderLayout.NORTH); libraryPanel.add(libraryScrollPane, BorderLayout.CENTER); // Split pane between tree and description final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); splitPane.setBorder(null); splitPane.setLeftComponent(libraryPanel); splitPane.setRightComponent(infoPane); splitPane.setDividerSize(3); splitPane.setDividerLocation(libraryTree.getPreferredSize().width + 100); add(splitPane, BorderLayout.CENTER); } /** * Gets the node library this viewer references. * * @return the node library */ public NodeLibrary getLibrary() { return library; } /** * Sets the node library this viewer will reference. * * @param library the library */ public void setLibrary(NodeLibrary library) { if(this.library != library) { if(this.library != null) this.library.removeNodeLibraryListener((NodeLibraryTreeModel)libraryTree.getModel()); this.library = library; this.model = new NodeLibraryTreeModel(library); libraryTree.setModel(model); for(int row = libraryTree.getRowCount() - 1; row >= 0; --row) libraryTree.expandRow(row); if(this.library != null) this.library.addNodeLibraryListener(model); } } /** * Gets HTML text representing specified info. * * @param info the node info * * @return HTML-ified version of the info */ private String getHTMLForNodeInfo(NodeData info) { final StringBuilder builder = new StringBuilder(); builder.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"); builder.append(String.format(" <h1>%s</h1><p>%s</p>%n", info.name, info.description)); builder.append("</html>\n"); return builder.toString(); } // // DragGestureListener // private final DragGestureListener gestureListener = new DragGestureListener() { @Override public void dragGestureRecognized(DragGestureEvent dge) { if(libraryTree.getSelectionPath() == null) return; final DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)libraryTree.getSelectionPath().getLastPathComponent(); if(selectedNode != null) { final Object selectedObject = selectedNode.getUserObject(); if(selectedObject != null && selectedObject instanceof NodeData) { final String txt = selectedObject.toString(); final Font font = getFont().deriveFont(Font.BOLD); final FontRenderContext frc = new FontRenderContext(null, true, true); final Rectangle2D bounds = font.getStringBounds(txt, frc); final int txtw = (int)(bounds.getWidth() + 20); final int txth = (int)(bounds.getHeight() + 10); final BufferedImage DRAG_IMG = new BufferedImage(txtw, txth, BufferedImage.TYPE_INT_ARGB); final Graphics2D g = DRAG_IMG.createGraphics(); { // Draw background g.setColor(new Color(255, 255, 150, 200)); g.fillRect(0, 0, txtw - 1, txth - 1); // Draw border g.setColor(Color.BLACK); g.drawRect(0, 0, txtw - 1, txth - 1); // Draw text g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setFont(font); g.setColor(Color.BLACK); final LineMetrics lm = font.getLineMetrics(txt, frc); final float txtx = (txtw - (float)bounds.getWidth()) * 0.5f; final float txty = (txth - (float)bounds.getHeight()) * 0.5f + lm.getAscent(); g.drawString(txt, txtx, txty); g.dispose(); } final Point p = new Point(DRAG_IMG.getWidth() / -2, DRAG_IMG.getHeight() / -2); final ObjectSelection sel = new ObjectSelection(selectedObject); dge.getDragSource().startDrag(dge, DragSource.DefaultCopyDrop, DRAG_IMG, p, sel, null); } } } }; }